package com.demo.auth.config;

import com.demo.auth.constant.UserDetailsServiceName;
import com.demo.auth.entity.AdminUserDetails;
import com.demo.auth.extension.PreAuthenticatedUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;

import javax.annotation.Resource;
import java.security.KeyPair;
import java.util.*;

/**
 * 认证服务配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;
    private InMemoryAuthorizationCodeServices inMemoryAuthorizationCodeServices;
    @Resource(name = "jdbcClientDetailsService")
    private ClientDetailsService clientDetailsService;

    /**
     * 多用户体系下，刷新token再次认证客户端ID和 UserDetailService 的映射Map
     */
    @Resource
    private Map<String, UserDetailsService> userDetailsServiceMap;
    @Resource(name = UserDetailsServiceName.ADMIN)
    private UserDetailsService userDetailsService;

    public AuthorizationServerConfig(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    /**
     * 配置OAuth2认证允许接入的客户端的信息
     * token刷新时间，密码加密方式，鉴权方式
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

    /**
     * 配置授权（authorization）以及令牌（token）的访问端点和令牌服务(token services)
     * 只有默认端点才会使用AuthorizationServerEndpointsConfigurer的配置
     * 默认端点
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        // 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
        List<TokenGranter> granterList = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));

        CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);

        endpoints.authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .userDetailsService(userDetailsService)
//                .tokenGranter(compositeTokenGranter)
//                .tokenServices(tokenServices(endpoints))
                .authorizationCodeServices(authorizationCodeServices());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.allowFormAuthenticationForClients()
                .tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("permitAll()");
    }

    /**
     * 设置授权码存储方式，这里将授权码保存到内存。如果服务有多台机器时，建议使用redis或spring security提供的JdbcAuthorizationCodeServices
     * @return AuthorizationCodeServices
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(){
        // 防止创建多个InMemoryAuthorizationCodeServices实例，因为configure方法内有直接调用改方法获取实例，而不是通过spring注入获取实例
        if (Objects.isNull(inMemoryAuthorizationCodeServices)) {
            this.inMemoryAuthorizationCodeServices = new InMemoryAuthorizationCodeServices();
        }
        return inMemoryAuthorizationCodeServices;
    }

    /**
     * 从classpath下的**库中获取**对(公钥+私钥)
     */
    @Bean
    public KeyPair keyPair() {
        char[] clientPwd = "123456".toCharArray();
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("ffzs-jwt.jks"), clientPwd);
        return factory.getKeyPair("ffzs-jwt", clientPwd);
    }

    /**
     * JWT内容增强
     * JWT负载信息默认是固定的，如果想自定义添加一些额外信息，需要实现TokenEnhancer的enhance方法将附加信息添加到access_token中。
     */
    private TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            // UserDetails对象来源于UserDetailsService.loadUserByUsername
            AdminUserDetails user = (AdminUserDetails) authentication.getUserAuthentication().getPrincipal();
            // 将用户名，id附加到access_token
            Map<String, Object> map = Map.of("userName", user.getUsername(),
                    "userId", user.getId(),
                    "authModel", user.getAuthModel(),
                    "createTime", System.currentTimeMillis());
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
            return accessToken;
        };
    }

    /**
     * 使用非对称加密算法对token签名
     */
    private JwtAccessTokenConverter jwtAccessTokenConverter() {
        // JwtAccessTokenConverter是生成token的转换器，可以实现指定token的生成方式(JWT)和对JWT进行签名。
        // 签名实际上是生成一段标识(JWT的Signature部分)作为接收方验证信息是否被篡改的依据
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }

    private DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancerChain);

        // 刷新token模式下，重写预认证提供者替换其AuthenticationManager，可自定义根据客户端ID和认证方式区分用户体系获取认证用户信息
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(userDetailsServiceMap));
        tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));

        /*
         * refresh_token有两种使用方式：重复使用(true)、非重复使用(false)，默认为true
         *  1 重复使用：access_token过期刷新时， refresh_token过期时间未改变，仍以初次生成的时间为准
         *  2 非重复使用：access_token过期刷新时， refresh_token过期时间延续，在refresh_token有效期内刷新便永不失效达到无需再次登录的目的
         */
        tokenServices.setReuseRefreshToken(true);
        return tokenServices;

    }
}
